//write.cpp
//author: aandriyash

#include "utils.h"

#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>

#include <iostream>

int WriteToRAID0(
    FILE* pFile
    , int fileSize
    , int blockNumber
    , int totalDisks
    , int totalBlocks
    , int blockSize
)
{
    RETURN_IF_ERROR(Mapping(totalDisks, totalBlocks, blockSize, false));

    int fileBlockSize = fileSize / totalDisks; 
    int offset = (blockNumber - 1) * blockSize;

    for (int i = 1; i < totalDisks + 1; i++) 
    {
        if (i == totalDisks) 
        {
            fileBlockSize += fileSize % totalDisks;
        }
        fread(ppMappedDataArray[i] + offset, 1, fileBlockSize, pFile);
        if (fileBlockSize < blockSize) 
        {
            memset(ppMappedDataArray[i] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
        }
    }
    
    RETURN_IF_ERROR(Unmapping(totalDisks, totalBlocks, blockSize, false));
    
    return 0;
}


int WriteToRAID1(
    FILE* pFile
    , int fileSize
    , int blockNumber
    , int totalDisks
    , int totalBlocks
    , int blockSize
)
{
    RETURN_IF_ERROR(Mapping(totalDisks, totalBlocks, blockSize, false));

    int offset = (blockNumber - 1) * blockSize;

    for (int i = 1; i < totalDisks + 1; i++)
    {

        rewind(pFile);
        fread(ppMappedDataArray[i] + offset, 1, fileSize, pFile);
        if (fileSize < blockSize) 
        {
            memset(ppMappedDataArray[i] + offset + fileSize, 0, blockSize - fileSize);
        }
    }

    RETURN_IF_ERROR(Unmapping(totalDisks, totalBlocks, blockSize, false));
    
    return 0;
}

char * GenerateParity(
    char** ppForParity
    , int totalDisks
    , int fileSize
)
{
    int fileBlockSize = fileSize / (totalDisks - 1) + fileSize % (totalDisks - 1);
    char* result = new char[fileBlockSize];
    char XORres;

    for(int i = 0; i < fileBlockSize; i++)
    {
        XORres = 0;
        for(int j = 1; j < totalDisks; j++)
        {
            XORres = XORres xor ppForParity[j][i];
        }
        result[i] = XORres;
    }
    return result;
}

int WriteToRAID4(
    FILE* pFile
    , int fileSize
    , int blockNumber
    , int totalDisks
    , int totalBlocks
    , int blockSize
) 
{
    RETURN_IF_ERROR(Mapping(totalDisks, totalBlocks, blockSize, false));

    int fileBlockSize = fileSize / (totalDisks - 1); //parts of pFile for writing to blocks;
    int offset = (blockNumber - 1) * blockSize;
    char** ppForParity = new char*[totalDisks];

    for (int i = 1; i < totalDisks; i++) 
    {
        if (i == totalDisks - 1) 
        {
            fileBlockSize += fileSize % (totalDisks - 1);
        }
        
        //write pFile to mappedData with offset
        fread(ppMappedDataArray[i] + offset, 1, fileBlockSize, pFile);
        if (fileBlockSize < blockSize) 
        {
            //filling the rest of block with '0'
            memset(ppMappedDataArray[i] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
        }
        ppForParity[i] = ppMappedDataArray[i] + offset;
    }
    
    //write parity to mappedData with offset
    char* data = GenerateParity(ppForParity, totalDisks, fileSize);
    memcpy(ppMappedDataArray[totalDisks] + offset, data, fileBlockSize);
    if (fileBlockSize < blockSize) 
    {
        //filling the rest of block with '0'
        memset(ppMappedDataArray[totalDisks] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
    }
    
    RETURN_IF_ERROR(Unmapping(totalDisks, totalBlocks, blockSize, false));
    
    return 0;
}

int WriteToRAID3(
    FILE* pFile
    , int fileSize
    , int blockNumber
    , int totalDisks
    , int totalBlocks
    , int blockSize
)
{
    RETURN_IF_ERROR(Mapping(totalDisks, totalBlocks, blockSize, false));

    int fileBlockSize = fileSize / (totalDisks - 1) + 1; //parts of pFile for writing to blocks;
    int offset = (blockNumber - 1) * blockSize;
    char XORres;
    int size;

    for (int k = 0; k < fileBlockSize; k++) 
    {
        XORres = 0;
        for (int i = 1; i < totalDisks; i++) 
        {
            //write pFile to mappedData with offset
            size = fread(ppMappedDataArray[i] + offset + k, 1, 1, pFile);
            if (size == 0) 
            {
                *(ppMappedDataArray[i] + offset + k) = 0;
            }
            XORres = XORres xor *(ppMappedDataArray[i] + offset + k);
        }
        
        //write parity byte to mappedData with offset
        *(ppMappedDataArray[totalDisks] + offset + k) = XORres;
    }

    if (fileBlockSize < blockSize) 
    {
        //filling the rest of blocks with '0'
        for (int i = 1; i <= totalDisks; i++) 
        {
            memset(ppMappedDataArray[i] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
        }
    }

    RETURN_IF_ERROR(Unmapping(totalDisks, totalBlocks, blockSize, false));
    
    return 0;
}

int WriteToRAID5(
    FILE* pFile
    , int fileSize
    , int blockNumber
    , int totalDisks
    , int totalBlocks
    , int blockSize
)
{
    RETURN_IF_ERROR(Mapping(totalDisks, totalBlocks, blockSize, false));

    int fileBlockSize = fileSize / (totalDisks - 1); //parts of pFile for writing to blocks;
    int offset = (blockNumber - 1) * blockSize;
    char** ppForParity = new char*[totalDisks];
    int parityDisk = totalDisks - (blockNumber - 1); //number of disk, which contains parity block
    int k = 0;

    for (int i = 1; i < totalDisks + 1; i++) 
    {
        if (parityDisk != i) 
        {
            k++;
            if (k == totalDisks - 1)
            {
                fileBlockSize += fileSize % (totalDisks - 1);
            }
            
            //write pFile to mappedData with offset
            fread(ppMappedDataArray[i] + offset, 1, fileBlockSize, pFile);
            if (fileBlockSize < blockSize) 
            {
                //filling the rest of block with '0'
                memset(ppMappedDataArray[i] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
            }
            ppForParity[k] = ppMappedDataArray[i] + offset;
        }
    }    
    //write parity to mappedData with offset
    char* data = GenerateParity(ppForParity, totalDisks, fileSize);
    memcpy(ppMappedDataArray[parityDisk] + offset, data, fileBlockSize);
    if (fileBlockSize < blockSize) 
    {
        //filling the rest of block with '0'
        memset(ppMappedDataArray[parityDisk] + offset + fileBlockSize, 0, blockSize - fileBlockSize);
    }

    RETURN_IF_ERROR(Unmapping(totalDisks, totalBlocks, blockSize, false));
    
    return 0;
}

int main(int argc, char* argv[]) {

    if (argc < 3) 
    {
        std::cout << "Not enough parameters\n";
        return 1;
    }

    RaidConfig raidConf;
    raidConf.Read();     
    
    RETURN_IF_ERROR_TRACE((IsDiskCrashed(raidConf.totalDisks, false)), ("Rebuild RAID.\n"));

    int blockNumber = atoi(argv[1]);
    if ((blockNumber == 0) || (blockNumber > raidConf.totalBlocks)) 
    {
        std::cout << "Number of blocks must be 1.." << raidConf.totalBlocks << "\n";
        return 1;
    }

    char* pFileName = argv[2];

    struct stat file_stat;
    
    int statf = stat(pFileName, &file_stat);
    
    if (0 != statf) 
    {
        std::cout << "Can't get statistics of pFile " << pFileName << ".\n";
        return 1;
    }
    
    if (file_stat.st_size > raidConf.blockSize) 
    {
        std::cout << "Size of pFile must be <= " << raidConf.blockSize << "\n";
        return 1;
    }

    FILE * pFile = fopen(pFileName, "rb+");
    if (NULL == pFile) 
    {
        std::cout << "Can't open pFile " << pFile << "\n";
        return 1;
    }
    int err = 0;
    switch (raidConf.raidLevel) 
    {
    case 0:
        err = WriteToRAID0(
                    pFile
                    , file_stat.st_size
                    , blockNumber
                    , raidConf.totalDisks
                    , raidConf.totalBlocks
                    , raidConf.blockSize
                );
        break;
    case 1:
        err = WriteToRAID1(
                    pFile
                    , file_stat.st_size
                    , blockNumber
                    , raidConf.totalDisks
                    , raidConf.totalBlocks
                    , raidConf.blockSize
                );
        break;
    case 3:
        err = WriteToRAID3(
                    pFile
                    , file_stat.st_size
                    , blockNumber
                    , raidConf.totalDisks
                    , raidConf.totalBlocks
                    , raidConf.blockSize
                );
        break;
    case 4:
        err = WriteToRAID4(
                    pFile
                    , file_stat.st_size
                    , blockNumber
                    , raidConf.totalDisks
                    , raidConf.totalBlocks
                    , raidConf.blockSize
                );
        break;
    case 5:
        err = WriteToRAID5(
                     pFile
                    , file_stat.st_size
                    , blockNumber
                    , raidConf.totalDisks
                    , raidConf.totalBlocks
                    , raidConf.blockSize
                );
        break;
    }
    
    fclose(pFile);
    return err;
}

